/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "TokenBucketTest.h"

#include <boost/optional/optional.hpp>
#include <stddef.h>
#include <algorithm>
#include <string>
#include <vector>

#include "gtest/gtest.h"
#include "utils/TokenBucket.h"

using namespace folly;

TEST(TokenBucket, ReverseTime)
{
    const double rate = 1000;
    TokenBucket tokenBucket(rate, rate * 0.01 + 1e-6, 0);
    size_t count = 0;
    while (tokenBucket.consume(1, 0.1)) {
        count += 1;
    }
    EXPECT_EQ(10, count);
    // Going backwards in time has no affect on the toke count (this protects
    // against different threads providing out of order timestamps).
    double tokensBefore = tokenBucket.available();
    EXPECT_FALSE(tokenBucket.consume(1, 0.09999999));
    EXPECT_EQ(tokensBefore, tokenBucket.available());
}

TEST_P(TokenBucketTest, sanity)
{
    std::pair<double, double> params = GetParam();
    double rate = params.first;
    double consumeSize = params.second;

    const double tenMillisecondBurst = rate * 0.010;
    // Select a burst size of 10 milliseconds at the max rate or the consume size
    // if 10 ms at rate is too small.
    const double burstSize = std::max(consumeSize, tenMillisecondBurst);
    TokenBucket tokenBucket(rate, burstSize, 0);
    double tokenCounter = 0;
    double currentTime = 0;
    // Simulate time advancing 10 seconds
    for (; currentTime <= 10.0; currentTime += 0.001) {
        EXPECT_FALSE(tokenBucket.consume(burstSize + 1, currentTime));
        while (tokenBucket.consume(consumeSize, currentTime)) {
            tokenCounter += consumeSize;
        }
        // Tokens consumed should exceed some lower bound based on rate.
        // Note: The token bucket implementation is not precise, so the lower bound
        // is somewhat fudged. The upper bound is accurate however.
        EXPECT_LE(rate * currentTime * 0.9 - 1, tokenCounter);
        // Tokens consumed should not exceed some upper bound based on rate.
        EXPECT_GE(rate * currentTime + 1e-6, tokenCounter);
    }
}

static std::vector<std::pair<double, double>> rateToConsumeSize = {
    {100, 1},
    {1000, 1},
    {10000, 1},
    {10000, 5},
};

INSTANTIATE_TEST_SUITE_P(TokenBucket, TokenBucketTest, ::testing::ValuesIn(rateToConsumeSize));

TEST(TokenBucket, drainOnFail)
{
    DynamicTokenBucket tokenBucket;

    // Almost empty the bucket
    EXPECT_TRUE(tokenBucket.consume(9, 10, 10, 1));

    // Request more tokens than available
    EXPECT_FALSE(tokenBucket.consume(5, 10, 10, 1));
    EXPECT_DOUBLE_EQ(1.0, tokenBucket.available(10, 10, 1));

    // Again request more tokens than available, but ask to drain
    EXPECT_DOUBLE_EQ(1.0, tokenBucket.consumeOrDrain(5, 10, 10, 1));
    EXPECT_DOUBLE_EQ(0.0, tokenBucket.consumeOrDrain(1, 10, 10, 1));
}

TEST(TokenBucket, returnTokensTest)
{
    DynamicTokenBucket tokenBucket;

    // Empty the bucket.
    EXPECT_TRUE(tokenBucket.consume(10, 10, 10, 5));
    // consume should fail now.
    EXPECT_FALSE(tokenBucket.consume(1, 10, 10, 5));
    EXPECT_DOUBLE_EQ(0.0, tokenBucket.consumeOrDrain(1, 10, 10, 5));

    // Return tokens. Return 40 'excess' tokens but they wont be available to
    // later callers.
    tokenBucket.returnTokens(50, 10);
    // Should be able to allocate 10 tokens again but the extra 40 returned in
    // previous call are gone.
    EXPECT_TRUE(tokenBucket.consume(10, 10, 10, 5));
    EXPECT_FALSE(tokenBucket.consume(1, 10, 10, 5));
}

TEST(TokenBucket, consumeOrBorrowTest)
{
    DynamicTokenBucket tokenBucket;

    // Empty the bucket.
    EXPECT_TRUE(tokenBucket.consume(10, 10, 10, 1));
    // consume should fail now.
    EXPECT_FALSE(tokenBucket.consume(1, 10, 10, 1));
    // Now borrow from future allocations. Each call is asking for 1s worth of
    // allocations so it should return (i+1)*1s in the ith iteration as the time
    // caller needs to wait.
    for (int i = 0; i < 10; ++i) {
        auto waitTime = tokenBucket.consumeWithBorrowNonBlocking(10, 10, 10, 1);
        EXPECT_TRUE(waitTime.is_initialized());
        EXPECT_DOUBLE_EQ((i + 1) * 1.0, *waitTime);
    }

    // No allocation will succeed until nowInSeconds goes higher than 11s.
    EXPECT_FALSE(tokenBucket.consume(1, 10, 10, 11));
}
